/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package xenon3d.scene;

import javax.media.opengl.GL2;
import xenon3d.vector.AxisAngle4f;
import xenon3d.vector.EulerAngle3f;
import xenon3d.vector.FMath;
import xenon3d.vector.Matrix3f;
import xenon3d.vector.Matrix4f;
import xenon3d.vector.Point3f;
import xenon3d.vector.Quat4f;
import xenon3d.vector.Vector3f;

/**
 * The transform class is a container for OpenGL ModelView matrix data.
 * @author Volker Everts
 * @version 0.1 - 26.08.2010: Created
 * @version 0.2 - 31.12.2011: Revison
 */
public class Transform extends NodeComponent {

    // <editor-fold defaultstate="collapsed" desc=" Private Constant Attributes ">

    /** The index value for matrix element m11. */
    private static final int M11 = 0;

    /** The index value for matrix element m21. */
    private static final int M21 = 1;

    /** The index value for matrix element m31. */
    private static final int M31 = 2;

    /** The index value for matrix element m41. */
    private static final int M41 = 3;

    /** The index value for matrix element m12. */
    private static final int M12 = 4;

    /** The index value for matrix element m22. */
    private static final int M22 = 5;

    /** The index value for matrix element m32. */
    private static final int M32 = 6;

    /** The index value for matrix element m42. */
    private static final int M42 = 7;

    /** The index value for matrix element m13. */
    private static final int M13 = 8;

    /** The index value for matrix element m23. */
    private static final int M23 = 9;

    /** The index value for matrix element m33. */
    private static final int M33 = 10;

    /** The index value for matrix element m43. */
    private static final int M43 = 11;

    /** The index value for matrix element m14. */
    private static final int M14 = 12;

    /** The index value for matrix element m24. */
    private static final int M24 = 13;

    /** The index value for matrix element m34. */
    private static final int M34 = 14;

    /** The index value for matrix element m44. */
    private static final int M44 = 15;

   // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc=" Private Fields ">

    /** The internal transformation matrix data that make up this transform object. */
    private float[] m = new float[16];

    /** The rotational transform component. */
    private AxisAngle4f rot = new AxisAngle4f();

    /** The positional transform component. */
    private Point3f pos = new Point3f();

    /** The uniform scaling factor. */
    private float scale = 1.0f;

    /** A flag indicating that the current transformation represents the identity transformation. */
    private boolean identity = true;

    /** A flag indicating that the internal matrix data is valid. */
    private boolean valid = true;

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc=" Initialization ">

    /**
     * Creates a new identity transform.
     */
    public Transform() {
        m[M11] = m[M22] = m[M33] = m[M44] = 1.0f;
    }

    /**
     * Creates a new transform with no rotation, unit scale and the specified
     * position coordinates.
     * @param x the x position coordinate
     * @param y the y position coordinate
     * @param z the z position coordinate
     */
    public Transform(float x, float y, float z) {
        m[M11] = m[M22] = m[M33] = m[M44] = 1.0f;
        m[M14] = x;
        m[M24] = y;
        m[M34] = z;
        pos.set(x, y, z);
        identity = pos.x == 0.0f && pos.y == 0.0f && pos.z == 0.0f;
    }

    /**
     * Creates a new Transform that is equivalent to the specified Matrix4f.
     * @param mat the Matrix4f object
     */
    public Transform(Matrix4f mat) {
        mat.get(m, 0);
        scale = mat.getTransform(rot, pos);
        checkIdentity();
    }

    /**
     * Creates a new Transform that is equivalent to the specified other
     * transform.
     * @param trans the other transform
     */
    public Transform(Transform trans) {
        System.arraycopy(trans.m, 0, m, 0, 16);
        rot.set(trans.rot);
        pos.set(trans.pos);
        scale = trans.scale;
        identity = trans.identity;
        valid = trans.valid;
    }

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc=" Public Properties (General) ">

    /**
     * Sets this transform so that it is equivalent to the specified Matrix4f.
     * @param mat the Matrix4f
     */
    public final void set(Matrix4f mat) {
        mat.get(m, 0);
        scale = mat.getTransform(rot, pos);
        checkIdentity();
        valid = true;
    }

    /**
     * Sets this transform to the value of the specified other transform.
     * @param trans the other transform
     */
    public final void set(Transform trans) {
        System.arraycopy(trans.m, 0, m, 0, 16);
        rot.set(trans.rot);
        pos.set(trans.pos);
        scale = trans.scale;
        identity = trans.identity;
        valid = trans.valid;
    }

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc=" Public Properties (Identity and Scale) ">

    /**
     * Resets this transform to the identity transform.
     */
    public final void setIdentity() {
        rot.set(0.0f, 1.0f, 0.0f, 0.0f);
        pos.set(0.0f, 0.0f, 0.0f);
        scale = 1.0f;
        identity = true;
        valid = false;
    }

    /**
     * Checks whether this transform represents an identity transform using
     * epsilon equality. If so, the identity flag is set.
     * @return true, if this transform is epsilon equal to the identity transform
     */
    public final boolean checkIdentity() {
        if (identity) return true;
        if (
                FMath.isUnit(scale, FMath.EPS) &&
                FMath.isUnit(rot,   FMath.EPS) &&
                FMath.isZero(pos.x, FMath.EPS) &&
                FMath.isZero(pos.y, FMath.EPS) &&
                FMath.isZero(pos.z, FMath.EPS)) setIdentity();
        return identity;
    }

    /**
     * Sets the value of this transform to a scale transform matrix with the
     * passed scale. Any preexisting rotation and translation will be reset to 0.
     * @param scale the uniform scale factor for this transform
     */
    public final void set(float scale) {
        setIdentity();
        this.scale = scale;
        identity = scale == 1.0f;
    }

    /**
     * Sets the uniform scale factor by factoring out the old scale and
     * replacing it with the new scale. Rotation and position remain unchanged.
     * @param scale the new scale factor
     * @throws IllegalArgumentException if s is negative or smaller than the
     * FMath epsilon value
     */
    public final void setScale(float scale) {
        if (scale < FMath.EPS) throw new IllegalArgumentException();
        this.scale = scale;
    }

    /**
     * Returns the uniform scale factor.
     * @return the uniform scale factor
     */
    public final float getScale() {
        return scale;
    }

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc=" Public Properties (Position) ">

    /**
     * Sets the value of this transform to a position transform matrix with the
     * passed position. Any preexisting rotation will be reset to 0, the scale
     * will be reset to 1.
     * @param p the position transform point
     */
    public final void set(Point3f p) {
        setIdentity();
        pos.set(p);
        identity = p.x == 0.0f && p.y == 0.0f && p.z == 0.0f;
    }

    /**
     * Sets the positional component of this transform to the specified values.
     * Rotation and scale remain unchanged.
     * @param x the x position coordinate
     * @param y the y position coordinate
     * @param z the z position coordinate
     */
    public final void setPosition(float x, float y, float z) {
        pos.set(x, y, z);
        if (identity) identity = x == 0.0f && y == 0.0f && z == 0.0f;
        valid = false;
    }

    /**
     * Sets the positinal component of this transform to the specified position.
     * Rotation and scale remain unchanged.
     * @param p the position point
     */
    public final void setPosition(Point3f p) {
        pos.set(p);
        if (identity) identity = p.x == 0.0f && p.y == 0.0f && p.z == 0.0f;
        valid = false;
    }

    /**
     * Stores the positional component of this transform in the supplied point
     * object.
     * @param result the position point
     */
    public final void getPosition(Point3f result) {
        result.set(pos);
    }

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc=" Public Properties (Rotation - Absolute) ">

    /**
     * Sets the value of this transform to a counter clockwise rotation about the
     * x axis. All of the non-rotational components are set as if this were an
     * identity transform.
     * @param angle the angle in degrees to rotate about the X axis
     */
    public final void rotX(float angle) {
        setIdentity();
        rot.set(1.0f, 0.0f, 0.0f, angle);
        identity = angle == 0.0f;
    }

    /**
     * Sets the value of this transform to a counter clockwise rotation about the
     * y axis. All non-rotational components are reset as if this were an
     * identity transform.
     * @param angle the angle to rotate about the Y axis
     */
    public final void rotY(float angle) {
        setIdentity();
        rot.set(0.0f, 1.0f, 0.0f, angle);
        identity = angle == 0.0f;
    }

    /**
     * Sets the value of this transform to a counter clockwise rotation about the
     * z axis. All non-rotational components are reset as if this were an
     * identity transform.
     * @param angle the angle to rotate about the Z axis
     */
    public final void rotZ(float angle) {
        setIdentity();
        rot.set(0.0f, 0.0f, 1.0f, angle);
        identity = angle == 0.0f;
    }

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc=" Public Properties (Rotation - EulerAngles) ">

    /**
     * Sets the value of this transform to the rotation matrix converted from
     * the euler angles provided; Any preexisting scale is reset to 1. The
     * translational component is preserved.
     * The euler rotation angles are applied first about the X, then Y then Z
     * axis. These rotations are applied using a static frame of reference.
     * In other words, the orientation of the Y rotation axis is not affected
     * by the X rotation and the orientation of the Z rotation axis is not
     * affected by the X or Y rotation.
     * @param e the EulerAngle3f
     */
    public final void setRotation(EulerAngle3f e) {
        rot.set(e);
        if (identity) identity = e.x == 0.0f && e.y == 0.0f && e.z == 0.0f;
        valid = false;
    }

    /**
     * Returns an euler rotation tuple that is equivalent to that set by
     * <code>setRotation(Tuple3f)</code>. Note that it does not necessarily
     * return the *same* Euler angles, but the rotation will be equivalent, i.e.
     * will have the same result when used to rotate a vector or node.
     * @param result the euler angles that are equivalent to the rotational
     * component of this transform.
     */
    public final void getRotation(EulerAngle3f result) {
        result.set(rot);
    }

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc=" Public Properties (Rotation - AxisAngle ">

    /**
     * Sets the value of this transform to the matrix conversion of the
     * (normalized) axis and the angle argument. Any preexisting scale is reset
     * to 1. The translational component is preserved.
     * @param angle the angle in degrees
     * @param axis the axis vector
     */
    public final void setRotation(Vector3f axis, float angle) {
        rot.set(axis, angle);
        if (identity) identity = angle == 0.0f;
        valid = false;
    }

    /**
     * Gets the axis of rotation and an angle that are equivalent to the axis
     * and angle pair set by <code>setRotation(float, Vector3f)</code>. Note
     * that it does not necessarily return the *same* angle and axis, but the
     * rotation will be equivalent, i.e. will have the same result when used to
     * rotate a vector or node.
     * @param axis the axis of rotation
     * @return the angle of rotation in degrees
     */
    public final float getRotation(Vector3f axis) {
        return rot.get(axis);
    }

    /**
     * Sets the value of this transform to the matrix conversion of the (single
     * precision) axis-angle. Any preexisting scale is reset to 1. The
     * translational component is preserved.
     * @param a the axis-angle object
     */
    public final void setRotation(AxisAngle4f a) {
        rot.set(a);
        if (identity) identity = rot.w == 0.0f;
        valid = false;
    }

    /**
     * Gets the axis-angle which is equivalent to the axis-angle set by
     * <code>setRotation(AxisAngle4f)</code>. Note that it does not necessarily
     * return the *same* axis-angle, but the rotation will be equivalent, i.e.
     * will have the same result when used to rotate a vector or node.
     * @param result the axis-angle
     */
    public final void getRotation(AxisAngle4f result) {
        result.set(rot);
    }

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc=" Public Properties (Rotation - Quaternion ">

    /**
     * Sets the value of this transform to the matrix conversion of the
     * (single precision) quaternion argument. Any preexisting scale is reset
     * to 1. The translational component is preserved.
     * @param q the quaternion to be converted
     */
    public final void setRotation(Quat4f q) {
        rot.set(q);
        if (identity) identity = rot.w == 0.0f;
        valid = false;
    }

    /**
     * Returns the normalized rotational component of this transform; the values
     * are placed into the Quat4f parameter.
     * @param result quaternion into which the rotation component is placed
     */
    public final void getRotation(Quat4f result) {
        result.set(rot);
    }

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc=" Public Properties (Rotation - Matrix) ">

    /**
     * Sets the normalized rotational component of this transform to the
     * specified rotation matrix. The translational component is preserved.
     * @param mat matrix which contains the rotational component
     */
    public final void setRotation(Matrix3f mat) {
        rot.set(mat);
        if (identity) identity = rot.w == 0.0f;
        valid = false;
    }

    /**
     * Returns the normalized rotational component of this matrix; the values
     * are placed into the Matrix3d parameter.
     * @param result matrix into which the rotational component is placed
     */
    public final void getRotation(Matrix3f result) {
        result.set(rot);
    }

    /**
     * Replaces the upper 3x3 matrix values of this transform with the  values in
     * the specified rotation matrix. The translational component is preserved.
     * @param mat the matrix that will be the new upper 3x3
     */
    public final void setRotationScale(Matrix3f mat) {
        rot.set(mat);
        scale = mat.getScale();
        if (identity) identity = rot.w == 0.0f && scale == 1.0f;
        valid = false;
    }

    /**
     * Returns the upper 3x3 values of this matrix and places them into the
     * result matrix
     * @param result the matrix that will hold the values
     */
    public final void getRotationScale(Matrix3f result) {
        result.set(rot);
        result.setScale(scale);
    }

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc=" Public Properties (Transformable - AxisAngle) ">

    /**
     * Sets the rotational and positional components of this transform as well
     * as its scaling factor.
     * @param a the axis angle of rotation
     * @param p the position point
     * @param s the uniform scaling factor
     */
    public void setTransform(AxisAngle4f a, Point3f p, float s) {
        rot.set(a);
        pos.set(p);
        scale = s;
        checkIdentity();
        valid = false;
    }

    /**
     * Gets the rotational and positional components of this transform as well
     * as its scaling factor.
     * @param a the result axis angle of rotation
     * @param p the result position point
     * @return the uniform scaling factor
     */
    public float getTransform(AxisAngle4f a, Point3f p) {
        a.set(rot);
        p.set(pos);
        return scale;
    }

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc=" Public Properties (Transformable - Quaternion) ">

    /**
     * Sets the rotational and positional components of this transform as well
     * as its scaling factor.
     * @param q the rotation quaternion
     * @param p the position point
     * @param s the uniform scaling factor
     */
    public void setTransform(Quat4f q, Point3f p, float s) {
        rot.set(q);
        pos.set(p);
        scale = s;
        checkIdentity();
        valid = false;
    }

    /**
     * Gets the rotational and positional components of this transform as well
     * as its scaling factor.
     * @param q the result rotation quaternion
     * @param p the result position point
     * @return the uniform scaling factor
     */
    public float getTransform(Quat4f q, Point3f p) {
        q.set(rot);
        p.set(pos);
        return scale;
    }

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc=" Public Properties (Transformable - Matrix) ">

    /**
     * Sets the rotational and positional components of this transform as well
     * as an explicit scaling factor. Any preexisting scale in the rotation
     * matrix will be factored out before applying the specified scale.
     * @param mat the rotation matrix
     * @param p the position point
     * @param s the uniform scaling factor
     */
    public void setTransform(Matrix3f mat, Point3f p, float s) {
        rot.set(mat);
        pos.set(p);
        scale = s;
        checkIdentity();
        valid = false;
    }

    /**
     * Gets the rotational and positional components of this transform as well
     * as its scaling factor. The returned rotation matrix will be scaled by
     * the uniform scaling factor.
     * @param mat the result rotation quaternion
     * @param p the result position point
     * @return the uniform scaling factor
     */
    public float getTransform(Matrix3f mat, Point3f p) {
        mat.set(rot);
        p.set(pos);
        return scale;
    }

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc=" Public Methods (Transformation / Normalization) ">

    /**
     * Transforms the specified Point3f in place.
     * @param p the point to transform
     */
    public void transform(Point3f p) {
        transform(p, p);
    }

    /**
     * Transforms the specified Point3f p1, placing the result into Point3f p2.
     * @param p1 the point to transform (which is not changed)
     * @param p2 the result point
     */
    public void transform(Point3f p1, Point3f p2) {
        if (!valid) throw new IllegalStateException();
        float a = p1.x;
        float b = p1.y;
        float c = p1.z;
        p2.x = a * m[0] + b * m[4] + c * m[8] + m[12];
        p2.y = a * m[1] + b * m[5] + c * m[9] + m[13];
        p2.z = a * m[2] + b * m[6] + c * m[10] + m[14];
    }

    /**
     * Resets the scale of this transform to 1.
     */
    public final void normalize() {
        if (scale == 1.0f) return;
        setScale(1.0f);
    }

    public final void normalizeCP() {
        throw new UnsupportedOperationException("Not supported yet.");
    }

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc=" Protected OpenGL Methods ">

    /**
     * Begins a new transform block on the current OpenGL matrix stack, which
     * should be the ModelView matrix stack. The current OpenGL matrix is then
     * multiplied by this transform, thus combining this transform with the
     * currently active OpenGL ModelView transform. Use begin()..end() instead
     * of apply() in order to manage nesting of transforms.<p>
     * NOTE: if another OpenGL matrix stack is active when Xenon3D is calling
     * this method, the result is undefined.
     * @param gl the OpenGL gl interface object
     */
    protected static void begin(GL2 gl) {
        gl.glPushMatrix();
    }

    /**
     * Ends the currently active transform block and restores the current OpenGL
     * matrix stack to the state it was before begin() was called.
     * @param gl the OpenGL gl interface object
     */
    protected static void end(GL2 gl) {
        gl.glPopMatrix();
    }

    /**
     * Applies the current transform to the current OpenGL matrix stack.<p>
     * NOTE: the OpenGL matrix transform stack must always be the ModelView
     * matrix stack, though this is not specifically checked by this class.
     * If another OpenGL matrix stack is active when Xenon3D is calling this
     * method, the result is undefined.
     * @param gl the OpenGL gl interface object
     */
    protected void apply(GL2 gl) {
        gl.glMultMatrixf(m, 0);
    }

    // </editor-fold>

} // end class Transform